// Protocol Buffers for GWT
// Copyright 2008 Google Inc.  All rights reserved.
// Copyright 2010 Thomas Broyer  All rights reserved.
// http://code.google.com/p/protobuf-gwt/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package com.google.protobuf;

import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamReader;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Internal.EnumLite;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * @author t.broyer@ltgt.net Thomas Broyer
 * <br>Based on the initial work of:
 * @author kenton@google.com Kenton Varda
 */
public abstract class GeneratedMessage extends AbstractMessage {

  @SuppressWarnings("unchecked")
  public static abstract class Builder<BuilderType extends Builder<?>> extends
    AbstractMessage.Builder<BuilderType> {
    
    @Override
    public com.google.protobuf.Message.Builder addRepeatedField(
        FieldDescriptor field, Object value) {
      if (field.getContainingType() != getDescriptorForType()
          || !field.isRepeated()) {
        throw new IllegalArgumentException();
      }
      if (value == null) {
        throw new NullPointerException();
      }
      internalAddRepeatedField(field.getNumber(), value);
      return this;
    }
    
    @Override
    public com.google.protobuf.Message.Builder clearField(FieldDescriptor field) {
      if (field.getContainingType() != getDescriptorForType()) {
        throw new IllegalArgumentException();
      }
      internalClearField(field.getNumber());
      return this;
    }

    @Override
    public Map<FieldDescriptor, Object> getAllFields() {
      return internalGetResult().getAllFields();
    }
    
    @Override
    public Descriptor getDescriptorForType() {
      return internalGetResult().getDescriptorForType();
    }
    
    @Override
    public Object getField(FieldDescriptor field) {
      if (field.isRepeated()) {
        // The underlying list object is still modifiable at this point.
        // Make sure not to expose the modifiable list to the caller.
        return Collections.unmodifiableList(
          (List<?>) internalGetResult().getField(field));
      } else {
        return internalGetResult().getField(field);
      }
    }
    
    @Override
    public Object getRepeatedField(FieldDescriptor field, int index) {
      return internalGetResult().getRepeatedField(field, index);
    }
    
    @Override
    public int getRepeatedFieldCount(FieldDescriptor field) {
      return internalGetResult().getRepeatedFieldCount(field);
    }
    
    @Override
    public boolean hasField(FieldDescriptor field) {
      return internalGetResult().hasField(field);
    }
    
    @Override
    public boolean isInitialized() {
      return internalGetResult().isInitialized();
    }
    
    public BuilderType mergeFrom(SerializationStreamReader reader) throws SerializationException {
      while (true) {
        int tag = reader.readInt();
        if (tag == 0) {
          break;
        }
        final FieldDescriptor field = getDescriptorForType().findFieldByNumber(tag);
        if (field == null) {
          continue;
        }
        Object value;
        switch (field.getJavaType()) {
          case INT:
            value = reader.readInt();
            break;
          case LONG:
            value = reader.readLong();
            break;
          case FLOAT:
            value = reader.readFloat();
            break;
          case DOUBLE:
            value = reader.readDouble();
            break;
          case BOOLEAN:
            value = reader.readBoolean();
            break;
          case STRING:
            value = reader.readString();
            break;
          case BYTE_STRING:
            value = ByteString_CustomFieldSerializer.instantiate(reader);
            // FIXME: we know deserialize is a noop, but we could make this better overall
            break;
          case ENUM:
            value = field.getEnumType().findValueByNumber(reader.readInt());
            break;
          case MESSAGE:
            value = ((GeneratedMessage.Builder<?>) field.newBuilderForField()).mergeFrom(reader);
            break;
          default:
            throw new SerializationException();
        }
        if (field.isRepeated()) {
          addRepeatedField(field, value);
        } else {
          setField(field, value);
        }
      }
      return (BuilderType) this;
    }
    
    @Override
    public com.google.protobuf.Message.Builder newBuilderForField(
        FieldDescriptor field) {
      if (field.getContainingType() != getDescriptorForType()) {
        throw new IllegalArgumentException();
      }
      return field.newBuilderForField();
    }
    
    @Override
    public com.google.protobuf.Message.Builder setField(FieldDescriptor field,
        Object value) {
      if (field.getContainingType() != getDescriptorForType()) {
        throw new IllegalArgumentException();
      }
      if (value == null) {
        throw new NullPointerException();
      }
      internalSetField(field.getNumber(), value);
      return this;
    }
    
    @Override
    public com.google.protobuf.Message.Builder setRepeatedField(
        FieldDescriptor field, int index, Object value) {
      if (field.getContainingType() != getDescriptorForType() || !field.isRepeated()) {
        throw new IllegalArgumentException();
      }
      if (value == null) {
        throw new NullPointerException();
      }
      internalSetRepeatedField(field.getNumber(), index, value);
      return this;
    }
    
    protected abstract void internalAddRepeatedField(int tag, Object value);
    
    protected abstract void internalClearField(int tag);
    
    protected abstract GeneratedMessage internalGetResult();

    protected abstract void internalSetField(int tag, Object value);
    
    protected abstract void internalSetRepeatedField(int tag, int index, Object value);
  }

  @Override
  public Map<FieldDescriptor, Object> getAllFields() {
    return Collections.unmodifiableMap(getAllFieldsMutable());
  }

  @Override
  public Object getField(FieldDescriptor field) {
    if (field.getContainingType() != getDescriptorForType()) {
      throw new IllegalArgumentException();
    }
    return internalGetField(field.getNumber());
  }

  @Override
  public Object getRepeatedField(FieldDescriptor field, int index) {
    if (field.getContainingType() != getDescriptorForType() || !field.isRepeated()) {
      throw new IllegalArgumentException();
    }
    return internalGetRepeatedField(field.getNumber(), index);
  }

  @Override
  public int getRepeatedFieldCount(FieldDescriptor field) {
    if (field.getContainingType() != getDescriptorForType() || !field.isRepeated()) {
      throw new IllegalArgumentException();
    }
    return internalGetRepeatedFieldCount(field.getNumber());
  }

  @Override
  public boolean hasField(FieldDescriptor field) {
    if (field.getContainingType() != getDescriptorForType()) {
      throw new IllegalArgumentException();
    }
    if (field.isRepeated()) {
      throw new UnsupportedOperationException();
    }
    return internalHasField(field.getNumber());
  }
  
  public void writeTo(SerializationStreamWriter streamWriter) throws SerializationException {
    for (final FieldDescriptor field : getDescriptorForType().getFields()) {
      if (field.isRepeated()) {
        // TODO: serialize repeated fields
      } else {
        if (hasField(field)) {
          streamWriter.writeInt(field.getNumber());
          final Object value = getField(field);
          switch (field.getJavaType()) {
            case INT:
              streamWriter.writeInt((Integer) value);
              break;
            case LONG:
              streamWriter.writeLong((Long) value);
              break;
            case FLOAT:
              streamWriter.writeFloat((Float) value);
              break;
            case DOUBLE:
              streamWriter.writeDouble((Double) value);
              break;
            case BOOLEAN:
              streamWriter.writeBoolean((Boolean) value);
              break;
            case STRING:
              streamWriter.writeString((String) value);
              break;
            case BYTE_STRING:
              ByteString_CustomFieldSerializer.serialize(streamWriter, (ByteString) value);
              break;
            case ENUM:
              streamWriter.writeInt(((EnumLite) value).getNumber());
              break;
            case MESSAGE:
              ((GeneratedMessage) value).writeTo(streamWriter);
              break;
            default:
              throw new SerializationException();
          }
        }
      }
    }
  }
  
  protected abstract Object internalGetField(int tag);
  
  protected abstract Object internalGetRepeatedField(int tag, int index);
  
  protected abstract int internalGetRepeatedFieldCount(int tag);
  
  protected abstract boolean internalHasField(int tag);
  
  private Map<FieldDescriptor, Object> getAllFieldsMutable() {
    final TreeMap<FieldDescriptor, Object> result =
      new TreeMap<FieldDescriptor, Object>();
    final Descriptor descriptor = getDescriptorForType();
    for (final FieldDescriptor field : descriptor.getFields()) {
      if (field.isRepeated()) {
        final List<?> value = (List<?>) getField(field);
        if (!value.isEmpty()) {
          result.put(field, value);
        }
      } else {
        if (hasField(field)) {
          result.put(field, getField(field));
        }
      }
    }
    return result;
  }
}
